import { compileToFunctions } from 'vue-template-compiler'
import { createLocalVue, shallowMount } from 'packages/test-utils/src'
import Vue from 'vue'
import VueRouter from 'vue-router'
import ComponentWithChild from '~resources/components/component-with-child.vue'
import ComponentWithNameProp from '~resources/components/component-with-name-prop.vue'
import ComponentWithoutName from '~resources/components/component-without-name.vue'
import ComponentWithSlots from '~resources/components/component-with-slots.vue'
import ComponentWithVFor from '~resources/components/component-with-v-for.vue'
import Component from '~resources/components/component.vue'
import FunctionalComponent from '~resources/components/functional-component.vue'
import ComponentAsAClass from '~resources/components/component-as-a-class.vue'
import {
  functionalSFCsSupported,
  vueVersion,
  describeWithShallowAndMount,
  isRunningChrome
} from '~resources/utils'
import { itDoNotRunIf, itSkipIf } from 'conditional-specs'

describeWithShallowAndMount('find', mountingMethod => {
  itDoNotRunIf(
    mountingMethod.name === 'shallowMount' || vueVersion < 2.6,
    'returns a VueWrapper using a <router-view /> component',
    async () => {
      const localVue = createLocalVue()
      localVue.use(VueRouter)
      const TestComponentToFind = {
        render: h => h('div'),
        name: 'test-component-to-find'
      }
      const routes = [
        {
          path: '/a/b',
          name: 'ab',
          component: TestComponentToFind
        }
      ]
      const router = new VueRouter({ routes })
      const wrapper = mountingMethod(
        {
          template: '<router-view/>'
        },
        {
          localVue,
          router
        }
      )

      await router.push('/a/b')

      expect(wrapper.findComponent(TestComponentToFind).exists()).toBe(true)
    }
  )

  it('findComponent in functional component', () => {
    const Comp2 = {
      name: 'test',
      render(h) {
        return h('div', 'test')
      }
    }
    const Comp = {
      name: 'Comp',
      functional: true,
      render(h) {
        return h(Comp2)
      }
    }
    const wrapper = shallowMount(Comp)
    wrapper.getComponent(Comp2)
  })

  it('returns a Wrapper matching tag selector passed', () => {
    const compiled = compileToFunctions('<div><p></p><p></p></div>')
    const wrapper = mountingMethod(compiled)
    expect(wrapper.find('p').vnode).toBeTruthy()
    expect(wrapper.find('p').vm).toEqual(undefined)
  })

  it('returns Wrapper matching class selector passed', () => {
    const compiled = compileToFunctions('<div><div class="foo" /></div>')
    const wrapper = mountingMethod(compiled)
    expect(wrapper.find('.foo').vnode).toBeTruthy()
    expect(wrapper.find('.foo').vm).toEqual(undefined)
  })

  it('returns Wrapper matching class selector passed if nested in a transition', () => {
    const compiled = compileToFunctions('<transition><div /></transition>')
    const wrapper = mountingMethod(compiled)
    expect(wrapper.find('div').vnode).toBeTruthy()
  })

  itDoNotRunIf(
    isRunningChrome,
    'returns an array of Wrapper of elements matching class selector passed if they are declared inside a slot',
    () => {
      const wrapper = mountingMethod(ComponentWithSlots, {
        slots: {
          default: '<div class="foo"></div>'
        }
      })
      expect(wrapper.find('.foo').vnode).toBeTruthy()
    }
  )

  it('returns Wrapper matching class selector passed if they are declared inside a functional component', () => {
    const TestComponent = {
      functional: true,
      render(h, { props }) {
        return h('div', {}, [
          h('p', {
            class: {
              foo: true
            }
          }),
          h('p')
        ])
      },
      name: 'common'
    }
    const context = {
      data: { hello: true }
    }
    const wrapper = mountingMethod(TestComponent, {
      context
    })
    expect(wrapper.find('.foo').vnode).toBeTruthy()
  })

  it('returns Wrapper of elements matching id selector passed', () => {
    const compiled = compileToFunctions('<div><div id="foo" /></div>')
    const wrapper = mountingMethod(compiled)
    expect(wrapper.find('#foo').vnode).toBeTruthy()
  })

  it('returns matching extended component', () => {
    const ChildComponent = Vue.extend({
      template: '<div />',
      props: ['propA'],
      name: 'child-component'
    })
    const TestComponent = {
      template: '<child-component propA="hey" />',
      components: { ChildComponent }
    }
    const wrapper = mountingMethod(TestComponent)

    expect(wrapper.find(ChildComponent).name()).toEqual('child-component')
  })

  it('returns Wrapper of elements matching attribute selector passed', () => {
    const compiled = compileToFunctions('<div><a href="/"></a></div>')
    const wrapper = mountingMethod(compiled)
    expect(wrapper.find('[href="/"]').vnode).toBeTruthy()
  })

  it('throws an error when passed an invalid DOM selector', () => {
    const compiled = compileToFunctions('<div><a href="/"></a></div>')
    const wrapper = mountingMethod(compiled)
    const message =
      '[vue-test-utils]: wrapper.find() must be passed a valid CSS selector, Vue constructor, or valid find option object'
    const fn = () => wrapper.find('[href=&6"/"]')
    expect(fn).toThrow(message)
  })

  it('returns Wrapper of elements matching selector when descendant combinator passed', () => {
    const compiled = compileToFunctions(
      '<div><ul><li>list</li>item<li></li></ul></div>'
    )
    const wrapper = mountingMethod(compiled)
    expect(wrapper.find('div li').vnode).toBeTruthy()
  })

  it('returns Wrapper of elements matching selector with direct descendant combinator passed', () => {
    const compiled = compileToFunctions('<div><ul><ul></ul></ul></div>')
    const wrapper = mountingMethod(compiled)
    expect(wrapper.find('div > ul').vnode).toBeTruthy()
  })

  it('returns Wrapper of elements matching pseudo selector', () => {
    const compiled = compileToFunctions('<div><p></p><p></p></div>')
    const wrapper = mountingMethod(compiled)
    expect(wrapper.find('p:first-of-type').vnode).toBeTruthy()
  })

  it('returns Wrapper of Vue Components matching component', () => {
    const wrapper = mountingMethod(ComponentWithChild)
    expect(wrapper.find(Component).vnode).toBeTruthy()
  })

  it('returns Wrapper of Vue Components matching component using findComponent', () => {
    const wrapper = mountingMethod(ComponentWithChild)
    expect(wrapper.findComponent(Component).vnode).toBeTruthy()
  })

  it('findComponent returns top-level component when multiple components are matching', () => {
    const DeepNestedChild = {
      name: 'DeepNestedChild',
      template: '<div>I am deeply nested</div>'
    }
    const NestedChild = {
      name: 'NestedChild',
      components: { DeepNestedChild },
      template: '<deep-nested-child class="in-child" />'
    }
    const RootComponent = {
      name: 'RootComponent',
      components: { NestedChild },
      template: '<div><nested-child class="in-root"></nested-child></div>'
    }

    const wrapper = mountingMethod(RootComponent, { stubs: { NestedChild } })

    expect(wrapper.findComponent('.in-root').vm.$options.name).toEqual(
      'NestedChild'
    )

    // someone might expect DeepNestedChild here, but
    // we always return TOP component matching DOM element
    expect(wrapper.findComponent('.in-child').vm.$options.name).toEqual(
      'NestedChild'
    )
  })

  it('allows using findComponent on functional component', () => {
    const FuncComponentWithChildren = {
      functional: true,
      components: {
        ChildComponent: Component
      },
      render: h => h('div', {}, [h(Component)])
    }
    const wrapper = mountingMethod(FuncComponentWithChildren)
    expect(wrapper.findComponent(Component).exists()).toBe(true)
  })

  itSkipIf(isRunningChrome, 'returns Wrapper of class component', () => {
    const TestComponent = {
      template: `
        <div>
          <component-as-a-class />
        </div>
      `,
      components: {
        ComponentAsAClass
      }
    }
    const wrapper = mountingMethod(TestComponent)

    expect(wrapper.find(ComponentAsAClass).vnode).toBeTruthy()
  })

  itDoNotRunIf(
    mountingMethod.name === 'shallowMount',
    'follows DOM tree order',
    () => {
      const TestComponent = {
        template: `
      <main>
        <div class="1">
          <div class="1a"><div class="1aa"/></div><div class="1b" />
        </div>
        <component-2 />
        <div class="3" />
      </main>
      `,
        components: {
          'component-2': {
            template: '<div class="2" />'
          }
        }
      }
      const wrapper = mountingMethod(TestComponent)
      const wrappers = wrapper.findAll('div').wrappers
      const expectedClasses = ['1', '1a', '1aa', '1b', '2', '3']
      wrappers.forEach((w, i) =>
        expect(w.classes()).toContain(expectedClasses[i])
      )
    }
  )

  it('returns functional component', () => {
    if (!functionalSFCsSupported) {
      return
    }
    const TestComponent = {
      template: `
        <div>
          <functional-component />
        </div>
      `,
      components: {
        FunctionalComponent
      }
    }

    const wrapper = mountingMethod(TestComponent)

    expect(wrapper.find(FunctionalComponent).vnode).toBeTruthy()
    expect(wrapper.find(FunctionalComponent).vm).toEqual(undefined)
  })

  it('returns functional component with name', () => {
    const TestFunctionalComponent = {
      render: h => h('div'),
      functional: true,
      name: 'test-functional-component'
    }
    const TestComponent = {
      template: '<div><test-functional-component /></div>',
      components: {
        TestFunctionalComponent
      }
    }
    const wrapper = mountingMethod(TestComponent)
    if (vueVersion < 2.3) {
      const message =
        '[vue-test-utils]: find for functional components is not supported in Vue < 2.3'
      const fn = () => wrapper.find(TestFunctionalComponent)
      expect(fn).toThrow(message)
    } else {
      expect(wrapper.find(TestFunctionalComponent).exists()).toEqual(true)
    }
  })

  itSkipIf(
    vueVersion < 2.3,
    'returns functional component with name by name',
    () => {
      const TestFunctionalComponent = {
        render: h => h('div'),
        functional: true,
        name: 'test-functional-component'
      }
      const TestComponent = {
        template: '<div><test-functional-component /></div>',
        components: {
          TestFunctionalComponent
        }
      }
      const wrapper = mountingMethod(TestComponent)
      expect(
        wrapper.find({ name: 'test-functional-component' }).exists()
      ).toEqual(true)
    }
  )

  it('returns extended functional component', () => {
    const TestFunctionalComponent = Vue.extend({
      render: h => h('div'),
      functional: true
    })
    const TestComponent = {
      template: '<div><test-functional-component /></div>',
      components: {
        TestFunctionalComponent
      }
    }
    const wrapper = mountingMethod(TestComponent)
    if (vueVersion < 2.3) {
      const message =
        '[vue-test-utils]: find for functional components is not supported in Vue < 2.3'
      const fn = () => wrapper.find(TestFunctionalComponent)
      expect(fn).toThrow(message)
    } else {
      expect(wrapper.find(TestFunctionalComponent).exists()).toEqual(true)
    }
  })

  it('works correctly with innerHTML', () => {
    const TestComponent = {
      render(createElement) {
        return createElement('div', {
          domProps: {
            innerHTML: '<svg></svg>'
          }
        })
      }
    }
    const wrapper = mountingMethod(TestComponent)
    expect(wrapper.find('svg').find('svg').exists()).toEqual(true)
  })

  it('throws errror when searching for a component on an element Wrapper', () => {
    const TestComponent = {
      render(createElement) {
        return createElement('div', {
          domProps: {
            innerHTML: '<svg></svg>'
          }
        })
      }
    }
    const fn = () => mountingMethod(TestComponent).find('svg').find(Component)
    const message =
      '[vue-test-utils]: cannot find a Vue instance on a DOM node. The node you are calling find on does not exist in the VDom. Are you adding the node as innerHTML?'
    expect(fn).toThrow(message)
  })

  it('throws errror when using ref selector on an element Wrapper', () => {
    const TestComponent = {
      render(createElement) {
        return createElement('div', {
          domProps: {
            innerHTML: '<svg></svg>'
          }
        })
      }
    }
    const fn = () =>
      mountingMethod(TestComponent).find('svg').find({ ref: 'some-ref' })
    const message =
      '[vue-test-utils]: cannot find a Vue instance on a DOM node. The node you are calling find on does not exist in the VDom. Are you adding the node as innerHTML?'
    expect(fn).toThrow(message)
  })

  it('returns correct number of Vue Wrappers when component has a v-for', () => {
    const items = [{ id: 1 }, { id: 2 }, { id: 3 }]
    const wrapper = mountingMethod(ComponentWithVFor, { propsData: { items } })
    expect(wrapper.find(Component).vnode).toBeTruthy()
  })

  it('returns Wrapper matching selector using Wrapper as reference', () => {
    const wrapper = mountingMethod(ComponentWithChild)
    const div = wrapper.find('span')
    expect(div.find(Component).vnode).toBeTruthy()
  })

  it('selector works between mounts', () => {
    const ChildComponent = { template: '<div />' }
    const TestComponent = {
      template: '<child-component />',
      components: {
        ChildComponent
      }
    }
    const wrapper = mountingMethod(TestComponent)
    mountingMethod(ChildComponent)
    expect(wrapper.find(ChildComponent).vnode).toBeTruthy()
  })

  it('returns error Wrapper if Vue component is below Wrapper', () => {
    const AComponent = {
      render: () => {},
      name: 'a-component'
    }
    const localVue = createLocalVue()
    localVue.component('a-component', AComponent)
    const TestComponent = {
      template: `
        <div>
          <span />
          <a-component />
        </div>
      `
    }
    const wrapper = mountingMethod(TestComponent, { localVue })
    const span = wrapper.find('span')
    expect(span.find(AComponent).exists()).toEqual(false)
  })

  it('returns empty Wrapper with error if no nodes are found', () => {
    const wrapper = mountingMethod(Component)
    const selector = 'pre'
    const error = wrapper.find(selector)
    expect(error.exists()).toEqual(false)
    expect(error.selector).toEqual(selector)
  })

  it('returns empty Wrapper with error if no nodes are found when passed a component', () => {
    const wrapper = mountingMethod(Component)
    const error = wrapper.find(ComponentWithChild)
    expect(error.exists()).toEqual(false)
    expect(error.selector).toEqual(ComponentWithChild)
  })

  it('returns Wrapper of elements matching the ref in options object', () => {
    const compiled = compileToFunctions('<div><p ref="foo"></p></div>')
    const wrapper = mountingMethod(compiled)
    expect(wrapper.find({ ref: 'foo' })).toBeTruthy()
  })

  itSkipIf(vueVersion < 2.3, 'returns functional extended component', () => {
    const FunctionalExtendedComponent = Vue.extend({
      functional: true,
      render: h => h('div')
    })
    const TestComponent = {
      template: '<div><functional-extended-component /></div>',
      components: {
        FunctionalExtendedComponent
      }
    }
    const wrapper = mountingMethod(TestComponent)
    expect(wrapper.find(FunctionalExtendedComponent).exists()).toEqual(true)
  })

  it('returns Wrapper of Vue Component matching the extended component', () => {
    const BaseComponent = {
      template: '<div><a-component /></div>',
      components: {
        AComponent: Component
      }
    }
    const TestComponent = {
      extends: BaseComponent,
      name: 'test-component'
    }
    const wrapper = mountingMethod(TestComponent)
    expect(wrapper.find(TestComponent).exists()).toEqual(true)
    expect(wrapper.find(TestComponent).isVueInstance()).toEqual(true)
  })

  it('works for extended child components', () => {
    const ChildComponent = Vue.extend({
      template: '<div />'
    })
    const TestComponent = {
      template: '<child-component />',
      components: {
        ChildComponent
      }
    }
    const wrapper = mountingMethod(TestComponent)
    expect(wrapper.find(ChildComponent).exists()).toEqual(true)
  })

  it('returns a Wrapper matching a component name in options object', () => {
    const wrapper = mountingMethod(ComponentWithChild)
    expect(wrapper.find({ name: 'test-component' }).name()).toEqual(
      'test-component'
    )
  })

  it('returns a Wrapper matching a camelCase name option and a Pascal Case component name ', () => {
    const component = {
      name: 'CamelCase',
      render: h => h('div')
    }
    const wrapper = mountingMethod(component)
    expect(wrapper.find({ name: 'camelCase' }).name()).toEqual('CamelCase')
  })

  it('returns a Wrapper matching a kebab-case name option and a Pascal Case component name ', () => {
    const component = {
      name: 'CamelCase',
      render: h => h('div')
    }
    const wrapper = mountingMethod(component)
    expect(wrapper.find({ name: 'camel-case' }).name()).toEqual('CamelCase')
  })

  it('returns a Wrapper matching a Pascal Case name option and a kebab-casecomponent name ', () => {
    const component = {
      name: 'camel-case',
      render: h => h('div')
    }
    const wrapper = mountingMethod(component)
    expect(wrapper.find({ name: 'CamelCase' }).name()).toEqual('camel-case')
  })

  it('returns a Wrapper matching a component name if Component has a name prop', () => {
    const wrapper = mountingMethod(ComponentWithNameProp, {
      propsData: { name: 'prop1' }
    })
    expect(
      wrapper.findComponent({ name: 'component-with-name-prop' }).vnode
    ).toBeTruthy()
  })

  it('returns Wrapper of Vue Component matching the ref in options object', () => {
    const wrapper = mountingMethod(ComponentWithChild)
    expect(wrapper.find({ ref: 'child' }).isVueInstance()).toEqual(true)
  })

  it('throws an error when ref selector is called on a wrapper that is not a Vue component', () => {
    const compiled = compileToFunctions('<div><a href="/"></a></div>')
    const wrapper = mountingMethod(compiled)
    const a = wrapper.find('a')
    const message =
      '[vue-test-utils]: $ref selectors can only be used on Vue component wrappers'
    const fn = () => a.find({ ref: 'foo' })
    expect(fn).toThrow(message)
  })

  it('returns Wrapper matching ref selector in options object passed if nested in a transition', () => {
    const compiled = compileToFunctions(
      '<transition><div ref="foo"/></transition>'
    )
    const wrapper = mountingMethod(compiled)
    expect(wrapper.find({ ref: 'foo' })).toBeTruthy()
  })

  it('returns empty Wrapper with error if no nodes are found via ref in options object', () => {
    const wrapper = mountingMethod(Component)
    const selector = { ref: 'foo' }
    const error = wrapper.find(selector)
    expect(error.exists()).toEqual(false)
    expect(error.selector).toEqual(selector)
  })

  it('returns Wrapper matching component that has no name property', () => {
    const TestComponent = {
      template: `
        <div>
          <component-without-name />
        </div>
      `,
      components: {
        ComponentWithoutName
      }
    }
    const wrapper = mountingMethod(TestComponent)
    expect(wrapper.find(ComponentWithoutName).exists()).toEqual(true)
  })

  it('throws an error if selector is not a valid selector', () => {
    const wrapper = mountingMethod(Component)
    const invalidSelectors = [
      undefined,
      null,
      NaN,
      0,
      2,
      true,
      false,
      () => {},
      {},
      { name: undefined },
      { ref: 'foo', nope: true },
      []
    ]
    invalidSelectors.forEach(invalidSelector => {
      const message =
        '[vue-test-utils]: wrapper.find() must be passed a valid CSS selector, Vue constructor, or valid find option object'
      const fn = () => wrapper.find(invalidSelector)
      expect(fn).toThrow(message)
    })
  })

  it('handles unnamed components', async () => {
    const ChildComponent = {
      template: '<div />'
    }
    const TestComponent = {
      template: '<child-component v-if="renderChild" />',
      components: { ChildComponent },
      data: function () {
        return {
          renderChild: false
        }
      }
    }
    const wrapper = mountingMethod(TestComponent)

    expect(wrapper.find(ChildComponent).vnode).toBeUndefined()
    wrapper.vm.renderChild = true
    await Vue.nextTick()
    expect(wrapper.find(ChildComponent).vnode).toBeTruthy()
  })

  itDoNotRunIf(
    mountingMethod.name === 'shallowMount',
    'returns a VueWrapper instance by CSS selector if the element binds a Vue instance',
    () => {
      const childComponent = {
        name: 'bar',
        template: '<p/>'
      }
      const wrapper = mountingMethod({
        name: 'foo',
        template: '<div><child-component /></div>',
        components: { childComponent }
      })
      expect(wrapper.find('div').vm.$options.name).toEqual('foo')
      expect(wrapper.find('p').vm.$options.name).toEqual('bar')
    }
  )

  it('stores CSS selector', () => {
    const compiled = compileToFunctions('<div><p></p><p></p></div>')
    const wrapper = mountingMethod(compiled)
    const selector = 'p'
    const result = wrapper.find(selector)
    expect(result.selector).toEqual(selector)
  })

  it('stores ref selector', () => {
    const compiled = compileToFunctions('<div><p ref="foo"></p></div>')
    const wrapper = mountingMethod(compiled)
    const selector = { ref: 'foo' }
    const result = wrapper.find(selector)
    expect(result.selector).toEqual(selector)
  })

  it('stores component selector', () => {
    const wrapper = mountingMethod(ComponentWithChild)
    const selector = Component
    const result = wrapper.find(selector)
    expect(result.selector).toEqual(selector)
  })

  it('stores name selector', () => {
    const wrapper = mountingMethod(ComponentWithChild)
    const selector = { name: 'test-component' }
    const result = wrapper.find(selector)
    expect(result.selector).toEqual(selector)
  })
})
